pub mod models;

pub use reqwest::Error;
pub use url::Url;

use std::{collections::HashMap, fmt::Display, str::FromStr};

use chrono::{SecondsFormat, Utc};
use md5::compute;
use models::{
    bookmark_detail::BookmarkDetail,
    illust::Illust,
    illust_page::IllustPage,
    tag::{Tags, TrendTags},
    user_detail::UserDetail,
    user_illusts::UserIllusts,
    user_page::UserPage,
};
use reqwest::{
    self,
    header::{HeaderMap, CONTENT_TYPE, IF_MODIFIED_SINCE, REFERER},
    Client, StatusCode,
};
use serde::Deserialize;
use serde_json::Value;
use time::Instant;

const HASH_SALT: &str = "28c1fdd170a5204386cb1313c7077b34f83e4aaf4aa829ce78c231e05b0bae2c";
const BASE_API_URL_HOST: &str = "app-api.pixiv.net";
const ACCEPT_LANGUAGE: &str = "zh-CN";

#[derive(PartialEq, Default, Eq, Hash, Debug, Copy, Clone)]
pub enum Publicity {
    Public,
    Private,
    #[default]
    All,
}

#[derive(PartialEq, Debug, Copy, Clone, Eq)]
pub enum IllustType {
    Illust,
    Manga,
}

#[derive(PartialEq, Debug, Copy, Clone, Eq)]
pub enum RankingType {
    Day,
    Week,
    Month,
    DayMale,
    DayFemale,
    WeekOriginal,
    WeekRookie,
    DayR18,
    DayMaleR18,
    DayFemaleR18,
    WeekR18,
    WeekR18g,
}

impl Display for RankingType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(match self {
            RankingType::Day => "day",
            RankingType::Week => "week",
            RankingType::Month => "month",
            RankingType::DayMale => "day_male",
            RankingType::DayFemale => "day_female",
            RankingType::WeekOriginal => "week_original",
            RankingType::WeekRookie => "week_rookie",
            RankingType::DayR18 => "day_r18",
            RankingType::DayMaleR18 => "day_male_r18",
            RankingType::DayFemaleR18 => "day_female_r18",
            RankingType::WeekR18 => "week_r18",
            RankingType::WeekR18g => "week_r18g",
        })
    }
}

impl Display for IllustType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(match self {
            IllustType::Illust => "illust",
            IllustType::Manga => "manga",
        })
    }
}

impl Display for Publicity {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(match self {
            Publicity::Public => "public",
            Publicity::Private => "private",
            Publicity::All => "all",
        })
    }
}

#[derive(Clone, Debug)]
pub struct PixivClient {
    client: reqwest::Client,
    base_url: Url,
    refresh_token: String,
    // Надо будет поменять на auth_token: Option<String>, а то это ебаный пиздец
    auth_token: Option<String>,
    last_refresh: Instant,
}

/// Error returned on failure to authorize with pixiv.
#[derive(Debug)]
pub struct AuthError {
    reason: String,
}

#[derive(Debug)]
pub struct DownloadError {
    reason: String,
}

impl PixivClient {
    pub fn from_refresh_token(token: &str) -> Self {
        let time = Self::get_iso_date();
        let mut headers = HeaderMap::new();
        headers.insert("X-Client-Time", time.parse().unwrap());
        headers.insert(
            "X-Client-Hash",
            Self::get_hash(&(time + HASH_SALT)).parse().unwrap(),
        );
        headers.insert(
            "User-Agent",
            "PixivAndroidApp/5.0.155 (Android 10.0; Pixel C)"
                .parse()
                .unwrap(),
        );
        headers.insert("Accept-Language", ACCEPT_LANGUAGE.parse().unwrap());
        headers.insert("App-OS", "Android".parse().unwrap());
        headers.insert("App-OS-Version", "Android 10.0".parse().unwrap());
        headers.insert("App-Version", "5.0.166".parse().unwrap());
        headers.insert("Host", BASE_API_URL_HOST.parse().unwrap());

        let client_builder = Client::builder()
            .danger_accept_invalid_certs(true)
            .default_headers(headers);
        let client = client_builder.build().unwrap();
        PixivClient {
            client,
            base_url: Url::from_str("https://210.140.131.199").unwrap(),
            refresh_token: token.to_string(),
            auth_token: None,
            last_refresh: Instant::now(),
        }
    }

    fn get_iso_date() -> String {
        Utc::now().to_rfc3339_opts(SecondsFormat::Millis, false)
    }

    fn get_hash(s: &str) -> String {
        format!("{:?}", compute(s.as_bytes()))
    }

    pub async fn refresh_if_needed(&mut self) -> Result<(), AuthError> {
        if self.need_refresh() {
            self.refresh_auth().await?
        }
        Ok(())
    }

    pub fn need_refresh(&self) -> bool {
        self.last_refresh.elapsed().whole_hours() >= 1 || self.auth_token.is_none()
    }

    /// Refreshes the authentication. You should use this when your access token is close to expiring.
    /// Token valid during one hour
    pub async fn refresh_auth(&mut self) -> Result<(), AuthError> {
        const CLIENT_ID: &str = "MOBrBDS8blbauoSck0ZfDbtuzpyT";
        const CLIENT_SECRET: &str = "lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj";
        let mut data = HashMap::new();

        data.insert("client_id", CLIENT_ID);
        data.insert("client_secret", CLIENT_SECRET);
        data.insert("get_secure_url", "1");

        data.insert("grant_type", "refresh_token");
        data.insert("refresh_token", &self.refresh_token);

        let client = reqwest::Client::new();
        let Ok(res) = client
            .post("https://oauth.secure.pixiv.net/auth/token")
            .form(&data)
            .send()
            .await
        else {
            return Err(AuthError {
                reason: "Bad request!".to_string(),
            });
        };

        match res.status() {
            StatusCode::OK | StatusCode::MOVED_PERMANENTLY | StatusCode::FOUND => {
                // success
            }
            s => {
                return Err(AuthError {
                    reason: format!("Login failed. Check your refresh token. Response: {:?}", s),
                })
            }
        }

        let mut json_response: Value = res.json().await.unwrap();

        match json_response["response"]["access_token"].take() {
            Value::String(s) => {
                self.auth_token = Some(s);
            }
            _ => panic!("Failed to get access token."),
        }
        self.last_refresh = Instant::now();
        Ok(())
    }

    /// Allows you to get user recommendations
    pub async fn get_recommend(&self) -> Result<IllustPage, reqwest::Error> {
        let url = self
            .base_url
            .join("/v1/illust/recommended?filter=for_ios&include_ranking_label=true")
            .unwrap();
        let resp = self
            .client
            .get(url)
            .header(
                "Authorization",
                format!("Bearer {}", self.auth_token.as_ref().unwrap()),
            )
            .send()
            .await?;
        resp.json().await
    }

    /// Allows you to get a page with illustrations by url. You can use as an argument the next_url field from the IllustPage or UserPage structures.
    pub async fn get_illust_page_by_url(&self, url: &str) -> Result<IllustPage, reqwest::Error> {
        let resp = self
            .client
            .get(url)
            .header(
                "Authorization",
                format!("Bearer {}", self.auth_token.as_ref().unwrap()),
            )
            .send()
            .await?;
        resp.json().await
    }

    /// Allows you to get recommended users.
    pub async fn get_user_recommended(&self) -> Result<UserPage, reqwest::Error> {
        let url = self
            .base_url
            .join("/v1/user/recommended?filter=for_android")
            .unwrap();
        let resp = self
            .client
            .get(url)
            .header(
                "Authorization",
                format!("Bearer {}", self.auth_token.as_ref().unwrap()),
            )
            .send()
            .await?;
        resp.json().await
    }

    /// Allows you to get the user by id
    pub async fn get_user(&self, user_id: u32) -> Result<UserDetail, reqwest::Error> {
        let mut url = self.base_url.join("/v1/user/detail").unwrap();
        url.set_query(Some(&format!("user_id={user_id}&filter=for_android")));
        let resp = self
            .client
            .get(url)
            .header(
                "Authorization",
                format!("Bearer {}", self.auth_token.as_ref().unwrap()),
            )
            .send()
            .await?;
        resp.json().await
    }

    /// Allows you to get custom illustrations by id and illustration type
    pub async fn get_user_ilusts(
        &self,
        user_id: u32,
        illust_type: IllustType,
    ) -> Result<UserIllusts, reqwest::Error> {
        let mut url = self.base_url.join("v1/user/illusts").unwrap();
        url.set_query(Some(&format!(
            "user_id={user_id}&type={illust_type}&filter=for_android"
        )));
        let resp = self
            .client
            .get(url)
            .header(
                "Authorization",
                format!("Bearer {}", self.auth_token.as_ref().unwrap()),
            )
            .send()
            .await?;
        resp.json().await
    }

    /// Allows you to get custom illustrations by id, illustration type and shift
    pub async fn get_user_ilusts_offset(
        &self,
        user_id: u32,
        illust_type: IllustType,
        offset: u32,
    ) -> Result<UserIllusts, reqwest::Error> {
        let mut url = self.base_url.join("v1/user/illusts").unwrap();
        url.set_query(Some(&format!(
            "user_id={user_id}&type={illust_type}&offset={offset}&filter=for_android"
        )));
        let resp = self
            .client
            .get(url)
            .header(
                "Authorization",
                format!("Bearer {}", self.auth_token.as_ref().unwrap()),
            )
            .send()
            .await?;
        resp.json().await
    }

    /// Adds an illustration to bookmarks. Returns true if the operation succeeded and false otherwise.
    pub async fn post_like_illust(
        &self,
        illust_id: u32,
        restrict: Publicity,
        tags: Vec<&str>,
    ) -> Result<bool, reqwest::Error> {
        let url = self.base_url.join("/v2/illust/bookmark/add").unwrap();
        let mut form = HashMap::new();
        form.insert("illust_id", illust_id.to_string());
        form.insert("restrict", restrict.to_string());
        if !tags.is_empty() {
            let tag_string = tags
                .into_iter()
                .map(|s| s.trim())
                .collect::<Vec<&str>>()
                .join(" ");
            form.insert("tags[]", tag_string);
        }
        let resp = self
            .client
            .post(url)
            .header(
                "Authorization",
                format!("Bearer {}", self.auth_token.as_ref().unwrap()),
            )
            .form(&form)
            .send()
            .await?;
        if resp.text().await? == "{}" {
            return Ok(true);
        }
        Ok(false)
    }

    /// Removes the illustration from bookmarks. Allows you to get custom illustrations by id and illustration type
    pub async fn post_unlike_illust(&self, illust_id: u32) -> Result<bool, reqwest::Error> {
        let url = self.base_url.join("v1/illust/bookmark/delete").unwrap();
        let mut form = HashMap::new();
        form.insert("illust_id", illust_id.to_string());
        let resp = self
            .client
            .post(url)
            .header(
                "Authorization",
                format!("Bearer {}", self.auth_token.as_ref().unwrap()),
            )
            .form(&form)
            .send()
            .await?;
        if resp.text().await? == "{}" {
            return Ok(true);
        }
        Ok(false)
    }

    /// Get an illustration removed from bookmarks?
    pub async fn get_unlike_illust(&self, illust_id: u32) -> Result<Illust, reqwest::Error> {
        let mut url = self.base_url.join("/v1/illust/bookmark/delete").unwrap();
        url.set_query(Some(&format!("illust_id={illust_id}")));
        let resp = self
            .client
            .get(url)
            .header(
                "Authorization",
                format!("Bearer {}", self.auth_token.as_ref().unwrap()),
            )
            .send()
            .await?;
        resp.json().await
    }

    /// Allows you to get the ranking of illustrations. Date format "yyyy-mm-dd"
    pub async fn get_illust_ranking(
        &self,
        mode: RankingType,
        date: &str,
    ) -> Result<IllustPage, reqwest::Error> {
        let mut url = self.base_url.join("/v1/illust/ranking").unwrap();
        url.set_query(Some(&format!("mode={mode}&date={date}&filter=for_android")));
        let resp = self
            .client
            .get(url)
            .header(
                "Authorization",
                format!("Bearer {}", self.auth_token.as_ref().unwrap()),
            )
            .send()
            .await?;
        resp.json().await
    }

    /// Allows you to get the user's bookmarks
    pub async fn get_bookmarks_illust(
        &self,
        user_id: u32,
        restrict: Publicity,
        tag: Option<&str>,
    ) -> Result<IllustPage, reqwest::Error> {
        let mut url = self.base_url.join("/v1/user/bookmarks/illust").unwrap();
        let mut query = format!("user_id={user_id}&restrict={restrict}");
        if let Some(tag) = tag {
            query += &format!("tag={}", tag);
        }
        url.set_query(Some(&query));
        let resp = self
            .client
            .get(url)
            .header(
                "Authorization",
                format!("Bearer {}", self.auth_token.as_ref().unwrap()),
            )
            .send()
            .await?;
        resp.json().await
    }

    /// Remove user from followings
    pub async fn post_unfollow_user(&self, user_id: u32) -> Result<bool, reqwest::Error> {
        let url = self.base_url.join("/v1/user/follow/delete").unwrap();
        let mut form = HashMap::new();
        form.insert("user_id", user_id.to_string());
        let resp = self
            .client
            .post(url)
            .header(
                "Authorization",
                format!("Bearer {}", self.auth_token.as_ref().unwrap()),
            )
            .form(&form)
            .send()
            .await?;
        if resp.text().await? == "{}" {
            return Ok(true);
        }
        Ok(false)
    }

    /// Follow to user by id and restrict
    pub async fn post_follow_user(
        &self,
        user_id: u32,
        restrict: Publicity,
    ) -> Result<bool, reqwest::Error> {
        let url = self.base_url.join("/v1/user/follow/delete").unwrap();
        let mut form = HashMap::new();
        form.insert("user_id", user_id.to_string());
        form.insert("restrict", restrict.to_string());
        let resp = self
            .client
            .post(url)
            .header(
                "Authorization",
                format!("Bearer {}", self.auth_token.as_ref().unwrap()),
            )
            .form(&form)
            .send()
            .await?;
        if resp.text().await? == "{}" {
            return Ok(true);
        }
        Ok(false)
    }

    /// Get your followers
    pub async fn get_follow_user(
        &self,
        user_id: u32,
        restrict: Publicity,
    ) -> Result<UserPage, reqwest::Error> {
        let mut url = self.base_url.join("/v1/user/follower").unwrap();
        url.set_query(Some(&format!(
            "restrict={restrict}&user_id={user_id}&filter=for_android"
        )));
        let resp = self
            .client
            .get(url)
            .header(
                "Authorization",
                format!("Bearer {}", self.auth_token.as_ref().unwrap()),
            )
            .send()
            .await?;
        resp.json().await
    }

    /// Get last illusts from your followings
    pub async fn get_follow_illusts(
        &self,
        restrict: Publicity,
    ) -> Result<IllustPage, reqwest::Error> {
        let mut url = self.base_url.join("/v2/illust/follow").unwrap();
        url.set_query(Some(&format!("restrict={restrict}")));
        let resp = self
            .client
            .get(url)
            .header(
                "Authorization",
                format!("Bearer {}", self.auth_token.as_ref().unwrap()),
            )
            .send()
            .await?;
        resp.json().await
    }

    /// Allows you to get users subscribed by the user with the specified id
    pub async fn get_user_following(
        &self,
        user_id: u32,
        restrict: Publicity,
    ) -> Result<UserPage, reqwest::Error> {
        let mut url = self.base_url.join("/v1/user/following").unwrap();
        url.set_query(Some(&format!(
            "restrict={restrict}&user_id={user_id}&filter=for_android"
        )));
        let resp = self
            .client
            .get(url)
            .header(
                "Authorization",
                format!("Bearer {}", self.auth_token.as_ref().unwrap()),
            )
            .send()
            .await?;
        resp.json().await
    }

    pub async fn get_search_auto_complete_keywords(
        &self,
        word: &str,
    ) -> Result<Tags, reqwest::Error> {
        let mut url = self.base_url.join("/v2/search/autocomplete").unwrap();
        url.set_query(Some(&format!(
            "word={word}&merge_plain_keyword_results=true"
        )));
        let resp = self
            .client
            .get(url)
            .header(
                "Authorization",
                format!("Bearer {}", self.auth_token.as_ref().unwrap()),
            )
            .send()
            .await?;
        resp.json().await
    }

    pub async fn get_illust_trend_tags(&self) -> Result<TrendTags, reqwest::Error> {
        let url = self
            .base_url
            .join("/v1/trending-tags/illust?filter=for_android")
            .unwrap();
        let resp = self
            .client
            .get(url)
            .header(
                "Authorization",
                format!("Bearer {}", self.auth_token.as_ref().unwrap()),
            )
            .send()
            .await?;
        resp.json().await
    }

    /// search_target: ["partial_match_for_tags","exact_match_for_tags","title_and_caption"]
    /// sort: ["date_desc", "date_asc", "popular_desc"]
    /// bookmark_num - неизвестно что это
    /// В illustpage не хватает одного поля в этом ответе. Оно всегда одно и тоже search_span_limit: 31536000
    pub async fn get_search_illust(
        &self,
        word: &str,
        sort: Option<&str>,
        search_target: Option<&str>,
        start_date: Option<&str>,
        end_date: Option<&str>,
        bookmark_num: Option<u32>,
    ) -> Result<IllustPage, reqwest::Error> {
        let mut url = self.base_url.join("/v1/search/illust").unwrap();
        let mut query = format!("word={word}&filter=for_android&merge_plain_keyword_results=true");
        if let Some(sort) = sort {
            query += &format!("sort={}", sort);
        }
        if let Some(search_target) = search_target {
            query += &format!("search_target={}", search_target);
        }
        if let Some(start_date) = start_date {
            query += &format!("start_date={}", start_date);
        }
        if let Some(end_date) = end_date {
            query += &format!("end_date={}", end_date);
        }
        if let Some(bookmark_num) = bookmark_num {
            query += &format!("bookmark_num={}", bookmark_num);
        }
        url.set_query(Some(&query));
        let resp = self
            .client
            .get(url)
            .header(
                "Authorization",
                format!("Bearer {}", self.auth_token.as_ref().unwrap()),
            )
            .send()
            .await?;
        resp.json().await
    }

    pub async fn get_search_user(&self, word: &str) -> Result<UserPage, reqwest::Error> {
        let mut url = self.base_url.join("/v1/search/user").unwrap();
        url.set_query(Some(&format!("word={word}&filter=for_android")));
        let resp = self
            .client
            .get(url)
            .header(
                "Authorization",
                format!("Bearer {}", self.auth_token.as_ref().unwrap()),
            )
            .send()
            .await?;
        resp.json().await
    }

    pub async fn get_search_autocomplete(&self, word: &str) -> Result<Tags, reqwest::Error> {
        let mut url = self.base_url.join("/v2/search/autocomplete").unwrap();
        url.set_query(Some(&format!(
            "word={word}&merge_plain_keyword_results=true"
        )));
        let resp = self
            .client
            .get(url)
            .header(
                "Authorization",
                format!("Bearer {}", self.auth_token.as_ref().unwrap()),
            )
            .send()
            .await?;
        resp.json().await
    }

    pub async fn get_illust_related(&self, illust_id: u32) -> Result<IllustPage, reqwest::Error> {
        let mut url = self.base_url.join("/v2/illust/related").unwrap();
        url.set_query(Some(&format!("illust_id={illust_id}&filter=for_android")));
        let resp = self
            .client
            .get(url)
            .header(
                "Authorization",
                format!("Bearer {}", self.auth_token.as_ref().unwrap()),
            )
            .send()
            .await?;
        resp.json().await
    }

    pub async fn get_illust_bookmark_detail(
        &self,
        illust_id: u32,
    ) -> Result<BookmarkDetail, reqwest::Error> {
        let mut url = self.base_url.join("v2/illust/bookmark/detail").unwrap();
        url.set_query(Some(&format!("illust_id={illust_id}")));
        let resp = self
            .client
            .get(url)
            .header(
                "Authorization",
                format!("Bearer {}", self.auth_token.as_ref().unwrap()),
            )
            .send()
            .await?;
        #[derive(Deserialize)]
        struct A {
            bookmark_detail: BookmarkDetail,
        }
        let a: A = resp.json().await?;
        Ok(a.bookmark_detail)
    }

    pub async fn get_illust_detail(&self, illust_id: u32) -> Result<Illust, reqwest::Error> {
        let mut url = self.base_url.join("/v1/illust/detail").unwrap();
        url.set_query(Some(&format!("illust_id={illust_id}&filter=for_android")));
        let resp = self
            .client
            .get(url)
            .header(
                "Authorization",
                format!("Bearer {}", self.auth_token.as_ref().unwrap()),
            )
            .send()
            .await?;
        #[derive(Deserialize)]
        struct A {
            illust: Illust,
        }
        let a: A = resp.json().await?;
        Ok(a.illust)
    }

    pub async fn download_image(image_url: impl Into<Url>) -> anyhow::Result<bytes::Bytes> {
        // let image_url = image_url.set_host("i-cf.pximg.net");
        let image_url: Url = image_url.into();
        let image_str = image_url.to_string();
        let client = reqwest::Client::new();
        let resp = client
            .get(image_url)
            .header(REFERER, "https://www.pixiv.net/")
            .header(IF_MODIFIED_SINCE, "Anything date")
            .send()
            .await?;
        let Some(content_type) = resp.headers().get(CONTENT_TYPE) else {
            return Err(DownloadError {
                reason: "Response don't contains Content-Type header!".to_string(),
            })?;
        };

        let content_type = content_type.to_str().unwrap();
        if content_type.contains("image/jpeg") || content_type.contains("image/png") {
            Ok(resp.bytes().await?)
        } else {
            Err(DownloadError {
                reason: format!(
                    "Don't image in jpeg or png formats! content_type = {}, url = {}",
                    content_type, image_str
                ),
            })?
        }
    }
}

impl std::error::Error for AuthError {
    fn description(&self) -> &str {
        "An error occurred while trying to authenticate."
    }
}

impl std::fmt::Display for AuthError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(
            f,
            "An error occurred while trying to authenticate. Reason: {:?}",
            self.reason
        )
    }
}

impl std::error::Error for DownloadError {
    fn description(&self) -> &str {
        "An error occurred while trying to download image."
    }
}

impl std::fmt::Display for DownloadError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(
            f,
            "An error occurred while trying to download image. Reason: {:?}",
            self.reason
        )
    }
}

#[cfg(test)]
mod tests {
    use crate::PixivClient;

    #[test]
    fn test() {
        let rt = tokio::runtime::Runtime::new().unwrap();
        rt.block_on(async {
            let mut client = PixivClient::from_refresh_token(env!("REFRESH_TOKEN"));
            client.refresh_auth().await.unwrap();
            let resp = client.get_recommend().await.unwrap();
            println!("{:#?}", resp);
        });
    }
}
